/**
 * @file
 * @brief RTD：XWDS：SPI
 * @author
 * + 隐星魂 (Roy.Sun) <https://xwos.tech>
 * @copyright
 * + (c) 2015 隐星魂 (Roy.Sun) <https://xwos.tech>
 * > Licensed under the Apache License, Version 2.0 (the "License");
 * > you may not use this file except in compliance with the License.
 * > You may obtain a copy of the License at
 * >
 * >         http://www.apache.org/licenses/LICENSE-2.0
 * >
 * > Unless required by applicable law or agreed to in writing, software
 * > distributed under the License is distributed on an "AS IS" BASIS,
 * > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * > See the License for the specific language governing permissions and
 * > limitations under the License.
 */

#include "../board/std.h"
#include <string.h>
#include <xwos/osal/time.h>
#include <xwos/osal/sync/cond.h>
#include <xwos/osal/lock/spinlock.h>
#include <xwcd/ds/spi/master.h>
#include "Mcl.h"
#include "Spi.h"

struct mcalxwds_lpspim_driver_data {
        xwu8_t channel;
        xwid_t buscfgid;
        struct {
                struct xwos_splk lock;
                struct xwos_cond completion;
        } xfer;
};

static
xwer_t mcalxwds_lpspim_drv_probe(struct xwds_device * dev);

static
xwer_t mcalxwds_lpspim_drv_remove(struct xwds_device * dev);

static
xwer_t mcalxwds_lpspim_drv_start(struct xwds_device * dev);

static
xwer_t mcalxwds_lpspim_drv_stop(struct xwds_device * dev);

#if defined(XWCDCFG_ds_PM) && (1 == XWCDCFG_ds_PM)
static
xwer_t mcalxwds_lpspim_drv_suspend(struct xwds_device * dev);

static
xwer_t mcalxwds_lpspim_drv_resume(struct xwds_device * dev);
#endif

static
xwer_t mcalxwds_lpspim_drv_xfer(struct xwds_spim * spim, xwid_t cfgid,
                                const xwu8_t txd[], xwu8_t * rxb,
                                xwsz_t * size, xwtm_t to);

static
xwer_t mcalxwds_lpspim_drv_abort(struct xwds_spim * spim, xwtm_t to);

struct xwds_spim_driver mcalxwds_lpspim_drv = {
        .base = {
                .name = "mcalxwds.lpspim",
                .probe = mcalxwds_lpspim_drv_probe,
                .remove = mcalxwds_lpspim_drv_remove,
                .start = mcalxwds_lpspim_drv_start,
                .stop =  mcalxwds_lpspim_drv_stop,
#if defined(XWCDCFG_ds_PM) && (1 == XWCDCFG_ds_PM)
                .suspend = mcalxwds_lpspim_drv_suspend,
                .resume =  mcalxwds_lpspim_drv_resume,
#endif
        },
        .xfer = mcalxwds_lpspim_drv_xfer,
        .abort = mcalxwds_lpspim_drv_abort,
};

static
xwer_t mcalxwds_lpspim_drv_probe(struct xwds_device * dev)
{
        struct xwds_spim * spim;
        struct mcalxwds_lpspim_driver_data * drvdata;

        spim = xwds_cast(struct xwds_spim *, dev);
        drvdata = spim->dev.data;
        xwos_splk_init(&drvdata->xfer.lock);
        xwos_cond_init(&drvdata->xfer.completion);
        return XWOK;
}

static
xwer_t mcalxwds_lpspim_drv_remove(struct xwds_device * dev)
{
        struct xwds_spim * spim;
        struct mcalxwds_lpspim_driver_data * drvdata;

        spim = xwds_cast(struct xwds_spim *, dev);
        drvdata = spim->dev.data;
        xwos_cond_fini(&drvdata->xfer.completion);
        return XWOK;
}

static
xwer_t mcalxwds_lpspim_drv_start(struct xwds_device * dev)
{
        XWOS_UNUSED(dev);
        return XWOK;
}

static
xwer_t mcalxwds_lpspim_drv_stop(struct xwds_device * dev)
{
        XWOS_UNUSED(dev);
        return XWOK;
}

#if defined(XWCDCFG_ds_PM) && (1 == XWCDCFG_ds_PM)
static
xwer_t mcalxwds_lpspim_drv_suspend(struct xwds_device * dev)
{
        return mcalxwds_lpspim_drv_stop(dev);
}

static
xwer_t mcalxwds_lpspim_drv_resume(struct xwds_device * dev)
{
        return mcalxwds_lpspim_drv_start(dev);
}
#endif

static
xwer_t mcalxwds_lpspim_drv_xfer(struct xwds_spim * spim, xwid_t cfgid,
                                const xwu8_t txd[], xwu8_t * rxb,
                                xwsz_t * size, xwtm_t to)
{
        struct mcalxwds_lpspim_driver_data * drvdata;
        xwsz_t xfsz;
        Std_ReturnType mcal_rc;
        Spi_SequenceType seq_rc;
        xwer_t rc;
        xwreg_t cpuirq;
        union xwos_ulock lock;
        xwsq_t lkst;

        drvdata = spim->dev.data;
        lock.osal.splk = &drvdata->xfer.lock;
        xfsz = *size;
#if defined(CPUCFG_DCACHE) && (1 == CPUCFG_DCACHE)
        xwu32_t org = XWBOP_ROUND((xwu32_t)txd,
                                  CPUCFG_L1_CACHELINE_SIZE);
        xwu32_t end = XWBOP_ALIGN((xwu32_t)txd + (xwu32_t)xfsz,
                                  CPUCFG_L1_CACHELINE_SIZE);
        Mcl_CacheCleanByAddr(MCL_CACHE_DATA, false, org, end - org);
#endif
        xwos_splk_lock_cpuirqsv(&drvdata->xfer.lock, &cpuirq);
        mcal_rc = Spi_SetupEB(drvdata->channel, (xwu8_t *)txd, (xwu8_t *)rxb, xfsz);
        if (E_OK != mcal_rc) {
                rc = -EOVERFLOW;
                goto err_bufsize;
        }
        mcal_rc = Spi_AsyncTransmit(cfgid);
        if (E_OK != mcal_rc) {
                rc = -EIO;
                goto err_transmit;
        }
        drvdata->buscfgid = cfgid;
        rc = xwos_cond_wait_to(&drvdata->xfer.completion, lock, XWOS_LK_SPLK,
                               NULL, to, &lkst);
        if (XWOK == rc) {
                XWOS_BUG_ON((xwsq_t)XWOS_LKST_UNLOCKED == lkst);
                seq_rc = Spi_GetSequenceResult(cfgid);
                xwos_splk_unlock_cpuirqrs(&drvdata->xfer.lock, cpuirq);
                switch (seq_rc) {
                case SPI_SEQ_OK:
#if defined(CPUCFG_DCACHE) && (1 == CPUCFG_DCACHE)
                        org = XWBOP_ROUND((xwu32_t)rxb,
                                          CPUCFG_L1_CACHELINE_SIZE);
                        end = XWBOP_ALIGN((xwu32_t)rxb + (xwu32_t)xfsz,
                                          CPUCFG_L1_CACHELINE_SIZE);
        Mcl_CacheInvalidateByAddr(MCL_CACHE_DATA, org, end - org);
#endif
                        rc = XWOK;
                        break;
                case SPI_SEQ_PENDING:
                        rc = -EBUSY;
                        break;
                case SPI_SEQ_FAILED:
                        rc = -EIO;
                        break;
                case SPI_SEQ_CANCELLED:
                        rc = -ECANCELED;
                        break;
                }
        } else {
                if ((xwsq_t)XWOS_LKST_LOCKED == lkst) {
                        xwos_splk_unlock_cpuirqrs(&drvdata->xfer.lock, cpuirq);
                } else {
                        xwos_cpuirq_restore_lc(cpuirq);
                }
        }
        return rc;

err_transmit:
err_bufsize:
        xwos_splk_unlock_cpuirqrs(&drvdata->xfer.lock, cpuirq);
        return rc;
}

static
xwer_t mcalxwds_lpspim_drv_abort(struct xwds_spim * spim, xwtm_t to)
{
        struct mcalxwds_lpspim_driver_data * drvdata;
        xwreg_t cpuirq;

        XWOS_UNUSED(to);
        drvdata = spim->dev.data;
        xwos_splk_lock_cpuirqsv(&drvdata->xfer.lock, &cpuirq);
        Spi_Cancel(drvdata->buscfgid);
        xwos_splk_unlock_cpuirqrs(&drvdata->xfer.lock, cpuirq);
        xwos_cond_broadcast(&drvdata->xfer.completion);
        return XWOK;
}

/******** ******** lpspi0m ******** ********/
struct mcalxwds_lpspim_driver_data mcalxwds_lpspi0m_drvdata = {
        .channel = 0,
        .buscfgid = 0,
};

struct xwds_spim mcalxwds_lpspi0m = {
        /* attributes */
        .dev = {
                .name = "mcalxwds.lpspi.m",
                .id = 0,
                .resources = NULL,
                .drv = xwds_cast(struct xwds_driver *, &mcalxwds_lpspim_drv),
                .data = (void *)&mcalxwds_lpspi0m_drvdata,
        },
};

/******** ******** Callback ******** ********/
struct xwds_spim * const mcalxwds_lpspim_devices[] = {
        [0] = &mcalxwds_lpspi0m,
};

void mcalxwds_lpspim_callback(xwu8_t event, xwu8_t data)
{
        struct xwds_spim * spim;
        struct mcalxwds_lpspim_driver_data * drvdata;
        xwreg_t cpuirq;
        Spi_SequenceType seq_rc;

        XWOS_UNUSED(event);
        spim = mcalxwds_lpspim_devices[data];
        drvdata = spim->dev.data;
        xwos_splk_lock_cpuirqsv(&drvdata->xfer.lock, &cpuirq);
        seq_rc = Spi_GetSequenceResult(drvdata->buscfgid);
        if (SPI_SEQ_CANCELLED != seq_rc) {
                xwos_splk_unlock_cpuirqrs(&drvdata->xfer.lock, cpuirq);
                xwos_cond_broadcast(&drvdata->xfer.completion);
        } else {
                xwos_splk_unlock_cpuirqrs(&drvdata->xfer.lock, cpuirq);
        }
}
